A função do sistema operacional é fornecer aos programas do usuário um modelo do computador melhor, mais simples e mais limpo, assim como lidar com o gerenciamento de todos os recursos mencionados. A maioria dos computadores tem dois modos de operação: modo núcleo e modo usuário.
O sistema operacional opera em modo núcleo (também chamado modo supervisor). Nesse modo, ele tem acesso completo a todo o hardware e *pode executar qualquer instrução que a máquina for capaz de executar.*
Observe, na figura a seguir, uma visão geral simplificada dos principais componentes:
O resto do software opera em modo usuário, no qual apenas um subconjunto das instruções da máquina está disponível.
A diferença entre os modos exerce papel crucial na maneira como os sistemas operacionais funcionam.
Os sistemas operacionais são enormes, complexos e têm vida longa. O código-fonte do coração de um sistema operacional como Linux ou Windows tem cerca de cinco milhões de linhas.
Os sistemas operacionais realizam duas funções essencialmente não relacionadas: (1) fornecer a programadores de aplicativos (e programas aplicativos, claro) um conjunto de recursos abstratos limpo em vez de recursos confusos de hardware, e (2) gerenciar esses recursos de hardware
Sistemas operacionais transformam o feio em belo, como mostrado na figura:
Usando essa abstração, os programas podem criar, escrever, ler arquivos, sem ter que lidar com detalhes complexos de como o hardware funciona
O conceito de um sistema operacional como fundamentalmente fornecendo abstrações para programas aplicativos é uma visão top-down (abstração de cima para baixo). Uma visão alternativa, bottom-up (abstração de baixo para cima), sustenta que o sistema operacional está ali para gerenciar todas as partes de um sistema complexo.
O gerenciamento de recursos inclui a multiplexação (compartilhamento) de recursos de duas maneiras diferentes: no tempo e no espaço.
Multiplexado no tempo
Multiplexado no espaço
Um sistema operacional está intimamente ligado ao hardware do computador no qual ele é executado
A CPU, memória e dispositivos de E/S estão todos conectados por um sistema de barramento (bus) e comunicam-se uns com os outros sobre ele.
O “cérebro” do computador é a CPU. O ciclo básico de toda CPU é buscar a primeira instrução da memória, decodificá-la para determinar o seu tipo e operandos, executá-la.
Cada CPU tem um conjunto específico de instruções que ela consegue executar. Desse modo, um processador x86 não pode executar programas ARM.
As CPUs têm alguns registradores internos para armazenamento de variáveis e resultados temporários.
Os resgistradores são importantes por causa da multiplexação de tempo da CPU.
É o segundo principal componente em qualquer computador, o qual deve ser rápido ao extremo (mais rápida do que executar uma instrução, de maneira que a CPU não seja atrasada pela memória).
A camada superior consiste em registradores internos à CPU. Eles são feitos do mesmo material que a CPU e são, desse modo, tão rápidos quanto ela
Memória cache é uma parte da CPU. Atua como memória temporária para que seja recuperado rapidamente os dados, sem a necessidade de uma busca direta na memória principal
Mémoria principal tem por finalidade o armazenamento de instruções e dados de programas que serão ou estão sendo executados pela CPU.
Discos magnéticos são um tipo de memória não volátil de grande capacidade de armazenamento, usada para guardar informações (instruções e dados de programas) que não serão imediatamente usadas pela CPU.
DuckDB tem uma forma de consulta (vectorized or just-in-time query execution engines) que são processadas em lotes de dados que consistem em coleções de vetores, cada um contendo uma quantidade fixa de valores das colunas.
O resultado é um uso eficiente das operações no cache, mantendo os dados nas consultas tanto quanto possível no cache L1 e L2 muito rápido.
Memória virtual: A memória virtual proporciona a capacidade de executar programas maiores do que a memória física da máquina, rapidamente movendo pedaços entre a memória RAM e o disco.
Quando se instala o linux é possível definir o tamanho da partição do disco que será utilizado pela memória virtual (swap).
Já vimos que os sistemas operacionais apresentam duas funções: abstrações para os usuários e gerenciamento de recursos.
open()open() é executada em modo usuárioopen()open() é executado e retorna o resultado para o sistema operacional e sequencialmente para o usuário
Os desenvolvedores de aplicações projetam programas de acordo com uma interface de programação de aplicações (API — application programming interface).
A API especifica um conjunto de funções que estão disponíveis para um programador de aplicações, incluindo os parâmetros que são passados a cada função e os valores de retorno que o programador pode esperar.
Portable Operating System Interface (POSIX) é uma API para o sistema UNIX/Linux.
| Chamada | Descrição |
|---|---|
pid = fork() |
Criar um processo filho idêntico ao pai |
pid = waitpid(pid, statloc, options) |
Espera que um processo filho seja concluído |
s = execve(name, argv, environp) |
Substitui a imagem do núcleo de um processo |
exit(status) |
Conclui a execução do processo e devolve status |
forkfork_exemplo.cpp
Hello world!, process_id(pid) = 685
Hello world!, process_id(pid) = 686
waitpidwaitpid_exemplo.cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int i = 1;
int pid;
int waitpid_return;
int main ()
{
printf("process_id(pid) parent= %d\n", getpid());
pid = fork();
if (pid != 0)
{
for (int i = 1; i < 5; i++) {
waitpid_return = waitpid(pid, NULL, WNOHANG);
printf("%d, process_id(pid) child = %d, waitpid = %d\n", i, pid, waitpid_return);
}
}
else {
waitpid_return = waitpid(pid, NULL, WNOHANG);
printf("%d, process_id(pid) child = %d, waitpid = %d\n", i, pid, waitpid_return);}
return 0;
}execve
| Chamada | Descrição |
|---|---|
s = mkdir(name, mode) |
Cria um novo diretório |
s = rmdir(name) |
Remove um diretório |
s = link(name1, name2) |
Cria um nova entrada name1 apontando para name2 |
s = unlike(name) |
Remove uma entrada de diretório |
s = mount(special, name, flag) |
Monta um sistema de arquivos |
s = unmount(special) |
Desmonta um sistema de arquivos |
| Chamada | Descrição |
|---|---|
s = chdir(dirname) |
Altera o diretório de trabalho |
s = chmod(name, mode) |
Altera os bits de proteção de um arquivo |
s = kill(pid, signal) |
Envia um sinal para um processo |
seconds = time(&seconds) |
Obtém o tempo decorrido desde 01/01/1970 |
Os sistemas operacionais podem ser organizados de várias maneiras, dependendo de sua estrutura interna e de como eles gerenciam os recursos do computador.
Desse modo, um erro no driver de áudio fará que o som fique truncado ou pare, mas não derrubará o computador.
(a) Um hipervisor de tipo 1. (b) Um hipervisor de tipo 2 puro. (c) Um hipervisor de tipo 2 na prática.
O conceito mais central em qualquer sistema operacional é o processo: uma abstração de um programa em execução.
Sistemas operacionais precisam de alguma maneira para criar processos.
Quatro eventos principais fazem com que os processos sejam criados:
Inicialização do sistema. Alguns processos são de (1) primeiro plano, sendo processos que interagem com o usuário; (2) segundo plano, processos que não estão associados a um usuário (daemons)
Execução de uma chamada de sistema de criação de processo por um processo em execução. Um processo em execução emitirá chamadas de sistema para criar processos novos para ajudá-lo em seu trabalho
Solicitação de um usuário para criar um novo processo. Em sistemas interativos, os usuários podem começar um programa digitando um comando ou clicando duas vezes sobre um ícone.
Início de uma tarefa em lote. Pense no gerenciamento de estoque ao fim de um dia em uma cadeia de lojas, nesse caso usuários podem submeter tarefas em lote ao servidor (possivelmente de maneira remota).
No UNIX, há apenas uma chamada de sistema para criar um novo processo: fork. Essa chamada cria um clone exato do processo que a chamou. Após a fork, os dois processos, o pai e o filho, têm a mesma imagem de memória, as mesmas variáveis de ambiente e os mesmos arquivos abertos. Mas atuam de forma independente.
Como identificar processos filho?
$
parent.sh &
Usando:
$
pgrep -P <parent pid>
pgrep -lP <parent pid>
pstree -p <parent pid>
ps --ppid <parent pid>
ls /proc/<parent pid>/task
cat /proc/<parent pid>/task/<parent pid>/children
Após um processo ter sido criado, ele começa a ser executado e realiza qualquer que seja o seu trabalho. No entanto, nada dura para sempre, nem mesmo os processos. Cedo ou tarde, o novo processo terminará, normalmente devido a uma das condições a seguir:
Exemplos:
ctrl + c para interromper programas ou processos no primeiro plano!g++ foo.cpp e foo.cpp não existe no diretóriokill <pid>$
./grandparent.sh
ps -efj | egrep "PGID|children|parent"
kill -9 -<pgid>
grandparent.sh
parent.sh
$
curl -s https://openbible.com/textfiles/kjv.txt | \
grep "Exodus\s2" | \
head -4
São 3 Estados:
O nível mais baixo de um sistema operacional estruturado em processos controla interrupções e escalonamento. Acima desse nível estão processos sequenciais.
O escalonamento, isto é, decidir qual processo deve ser executado, quando e por quanto tempo, é um assunto importante; nós o examinaremos mais adiante neste capítulo. Muitos algoritmos foram desenvolvidos para tentar equilibrar as demandas concorrentes de eficiência para o sistema como um todo e justiça para os processos individuais.
Para implementar o modelo de processos, o sistema operacional mantém uma tabela (um arranjo de estruturas) chamada de tabela de processos, com uma entrada para cada um deles.
Cada processo tem um espaço de endereçamento e um único thread de controle. Na realidade, essa é quase a definição de um processo. Não obstante isso, em muitas situações, é desejável ter múltiplos threads de controle no mesmo espaço de endereçamento executando em quase paralelo, como se eles fossem (quase) processos separados (exceto pelo espaço de endereçamento compartilhado).
Por que alguém iria querer ter um tipo de processo dentro de um processo? Na realidade, há várias razões para a existência desses miniprocessos, chamados threads
A capacidade para as entidades em paralelo compartilharem um espaço de endereçamento e todos os seus dados entre si.
São mais leves do que os processos, eles são mais fáceis (isto é, mais rápidos) para criar e destruir do que os processos.
Quando há uma computação substancial e também E/S substancial, contar com threads permite que essas atividades se sobreponham.
Threads são úteis em sistemas com múltiplas CPUs, onde o paralelismo real é possível.
Um thread interage com o usuário e o outro lida com a reformatação em segundo plano. Tão logo a frase é apagada da página 1, o thread interativo diz ao de reformatação para reformatar o livro inteiro. Enquanto isso, o thread interativo continua a ouvir o teclado e o mouse e responde a comandos simples como rolar a página 1 enquanto o outro thread está trabalhando com afinco no segundo plano. Um terceiro thread pode fazer backups de disco sem interferir nos outros dois.
Deve ficar claro que ter três processos em separado não funcionaria aqui, pois todos os três threads precisam operar no documento. Ao existirem três threads em vez de três processos, eles compartilham de uma memória comum.
Todo thread pode acessar todo espaço de endereçamento de memória dentro do espaço de endereçamento do processo, um thread pode ler, escrever, ou mesmo apagar a pilha de outro thread. Não há proteção, porque (1) é impossível e (2) não seria necessário.
Para possibilitar que se escrevam programas com threads portáteis, o IEEE definiu um padrão para threads no padrão IEEE 1003.1c. O pacote de threads que ele define é chamado Pthreads.
Todos os threads têm determinadas propriedades. Cada um tem um identificador, um conjunto de registradores (incluindo o contador de programa), e um conjunto de atributos, que são armazenados em uma estrutura.
Algumas das chamadas de função do Pthreads:
multiple_threads.py
"""
Executando Múltiplas Threads
https://realpython.com/intro-to-python-threading/
"""
import logging
import threading
import time
def thread_function(name):
logging.info("Thread %s: starting, ID: %s", name, threading.get_native_id())
time.sleep(2)
logging.info("Thread %s: finishing", name)
if __name__ == "__main__":
format = "%(asctime)s: %(message)s"
logging.basicConfig(format=format, level=logging.INFO,
datefmt="%H:%M:%S")
threads = list()
for index in range(3):
logging.info("Main : create and start thread %d.", index)
x = threading.Thread(target=thread_function, args=(index,))
threads.append(x)
x.start()
for index, thread in enumerate(threads):
logging.info("Main : before joining thread %d.", index)
thread.join()
logging.info("Main : thread %d done", index)
Conflitos entre threads sobre o uso de uma variável global.
Threads podem ter variáveis globais individuais.